package ru.amse.jsynchro.kernel;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;

import ru.amse.jsynchro.fileSystem.FileSystem;
import ru.amse.jsynchro.fileSystem.LocalFileSystem;
import ru.amse.jsynchro.kernel.util.Path;

@SuppressWarnings("serial")
public class DirectoryTree implements IDirectoryTree, Serializable {
    private FileSystem myFS;
    private final TreeNode myRoot;
    private final String myPath;
    private boolean compareFileContent = true;
//    protected Logger logger = LogFactory.getClientConsoleLogger(true);
    
    public void setFS(FileSystem fs) {
        myFS = fs;
    }
    
    public FileSystem getFS() {
        return myFS;
    }

    public DirectoryTree(File file, FileSystem fs) {
        myFS = fs;
        if (!file.isDirectory()) {
            throw new IllegalArgumentException("arg must be a directory");
        }
        myRoot = new TreeNode(new NodeData(file, this, compareFileContent), null);
        myPath = file.getPath();
        makeSubtree(myRoot, file);
    }
    
    public void setCompareFileContent(boolean set) {
        compareFileContent = set;
        visitTree(myRoot, new SetFileContentStrategy(set));
    }
    
    private void makeSubtree(TreeNode rootNode, File rootFile) {
        File[] files = rootFile.listFiles();
        Arrays.sort(files, new Mycomp());
        if (files.length > 0) {
            TreeNode prev = null;
            prev = new TreeNode(new NodeData(files[0], this, compareFileContent), rootNode);
            if (files[0].isDirectory()) {
                makeSubtree(prev, files[0]);
            }
            rootNode.myLeftChild = prev;
            
            TreeNode node;
            for (int i = 1; i < files.length; i++) {
                node = new TreeNode(new NodeData(files[i], this, compareFileContent), rootNode);
//                node = new TreeNode(new NodeData(files[i], this), rootNode);
                prev.myNext = node;
                if (files[i].isDirectory()) {
                    makeSubtree(node, files[i]);
                }
                prev = node;
            }
        }
    }
    
    class Mycomp implements Comparator<File> {

        public int compare(File o1, File o2) {
            return o1.getName().compareTo(o2.getName());
        }
        
    }

    public DirectoryTree(NodeData rootData) {
        myFS = new LocalFileSystem();
        myRoot = new TreeNode(rootData, null);
        myPath = null;
    }
    
    public List<Difference> compare(IDirectoryTree second, boolean deletes) {
//        logger.info("first tree " + toString());
//        logger.info("second tree:  " + second.toString());
//        logger.info("in compare(IDirectoryTree second, boolean deletes)");
        List<Difference> diffs = new ArrayList<Difference>();
    	TreeNode fcur = myRoot;
    	TreeNode scur = null;
    	if (second != null) {
	        scur = ((DirectoryTree) second).myRoot;
    	}
        if (deletes) {
            compareSubTreesDel(fcur, scur, diffs);
        } else {
            compareSubTrees(fcur, scur, diffs);
        }
        return diffs;
    }
    
//    private List<Difference> compareWithNull(IDirectoryTree tree) {
//        List<Difference> diffs = new ArrayList<Difference>();
//        
//    }
    
    private void compareSubTrees(TreeNode fcur, TreeNode scur, List<Difference> diffs)  {
//        logger.info("compareSubTrees(TreeNode fcur, TreeNode scur, List<Difference> diffs)");
		fcur = fcur.myLeftChild;
		if (scur != null) {
		    scur = scur.myLeftChild;
		}
		while (fcur != null) {
		    // if second is ended, add other nodes of first
			if (scur == null) {
			    Difference ch = new Difference(fcur.getPath(), Operation.ADD, fcur.myLeftChild != null);
//			    logger.info("adding Difference: scur == null " + ch);
			    diffs.add(ch);
			    fcur = fcur.myNext;
			} else {
			    // if in the second found a node, which is more then in the first
			    if (fcur.myData.getName().compareTo(scur.myData.getName()) < 0) {
			        Difference ch = new Difference(fcur.getPath(), Operation.ADD, fcur.myLeftChild != null);
//			        logger.info("adding Difference: fcur.myData.getName().compareTo(scur.myData.getName()) < 0 " + ch);
    				diffs.add(ch);
    				fcur = fcur.myNext;
    			} else if (fcur.myData.getName().equals(scur.myData.getName())) {
    			    
//    			    if names are equal, but data is not 
    			    if (!fcur.myData.equals(scur.myData)) {
    			        Difference conflict = new Difference(fcur.getPath(), 
    			                Operation.CONFLICT, fcur.myLeftChild != null);
    			            diffs.add(conflict);
//    			        }
    			    } else {
//               if the first is not empty directory
    			        if (fcur.myLeftChild != null) {
                            compareSubTrees(fcur, scur, diffs);
                        }
    			    }
    			    compareAttrs(fcur, scur, diffs);
                    fcur = fcur.myNext;
                    scur = scur.myNext;
    			} else {
    			    scur = scur.myNext;
    			}
			}
		}	
	}
    
    
    
    private void compareAttrs(TreeNode fcur, TreeNode scur, List<Difference> diffs) {
        byte attrs = fcur.myData.compareAttrs(scur.myData);
        if ((attrs & 0x01) != 0) {
            diffs.add(new Difference(fcur.getPath(), 
                    Operation.ATTR_READ, fcur.myLeftChild != null));
        }
        if ((attrs & 0x02) != 0) {
            diffs.add(new Difference(fcur.getPath(), 
                    Operation.ATTR_WRITE, fcur.myLeftChild != null));
        }if ((attrs & 0x04) != 0) {
            diffs.add(new Difference(fcur.getPath(), 
                    Operation.ATTR_EXECUTE, fcur.myLeftChild != null));
        }if ((attrs & 0x08) != 0) {
            diffs.add(new Difference(fcur.getPath(), 
                    Operation.ATTR_HIDDEN, fcur.myLeftChild != null));
        }
    }

    private void compareSubTreesDel(TreeNode fcur, TreeNode scur, List<Difference> diffs) {
        fcur = fcur.myLeftChild;
        if (scur != null) {
            scur = scur.myLeftChild;
        }
        while ((fcur != null) || (scur != null)) {
            // if first is ended, remove other nodes of second
            if (fcur == null) {
                Difference ch = new Difference(scur.getPath(), Operation.REMOVE, scur.myLeftChild != null);
//                logger.info("adding Difference: " + ch);
                diffs.add(ch);
                scur = scur.myNext;
             // if the second is ended, add other nodes of the first
            } else if (scur == null) {
                Difference ch = new Difference(fcur.getPath(), Operation.ADD, fcur.myLeftChild != null);
//                logger.info("adding Difference: " + ch);
                diffs.add(ch);
                fcur = fcur.myNext;
             // compare nodes
                //if nodes are equal, nothing to do
            } else if (fcur.myData.equals(scur.myData)) {
//                if node is directory, compare it's subelements
                if (fcur.myLeftChild != null) {
                    compareSubTreesDel(fcur, scur, diffs);
                }
                fcur = fcur.myNext;
                scur = scur.myNext;
             // if name in the first less than in the second, add the first to the second
            } else if (fcur.myData.getName().compareTo(scur.myData.getName()) < 0) {
                Difference ch = new Difference(fcur.getPath(), Operation.ADD, fcur.myLeftChild != null);
//                logger.info("adding Difference: " + ch);
                diffs.add(ch);
                fcur = fcur.myNext;
//               if names in the second less than in the first, remove the element i
            } else {
                diffs.add(new Difference(scur.getPath(), Operation.REMOVE, scur.myLeftChild != null));
                scur = scur.myNext;
            }
        }
    }
    
	//path is the sequence of node names excluding a tree root
    public boolean add(Deque<String> path, NodeData data) {
    	if (!path.getLast().equals(data.getName())) {
    		throw new IllegalArgumentException("root name and name in the " +
    				"path must be equal");
    	}
        // TODO file.name == directory.name
        Iterator<String> iterator = path.iterator();
        String s;
        TreeNode node = myRoot;
        while (iterator.hasNext()) {
            s = iterator.next();
            node = searchAdd(node, s);
        }
        node.myData = data;
        return true;
    }
    
    public boolean remove(Deque<String> path) {
        Iterator<String> iterator = path.iterator();
        String s;
        TreeNode node = myRoot;
        while (iterator.hasNext()) {
            s = iterator.next();
            node = search(node, s);
            if (node == null) {
                return false;
            }
        }
        node = node.myParent;
        node.myLeftChild = null;
        return true;
    }
    
    public boolean update(Deque<String> path, NodeData data) {
        Iterator<String> iterator = path.iterator();
        String s;
        TreeNode node = myRoot;
        while (iterator.hasNext()) {
            s = iterator.next();
            node = search(node, s);
            if (node == null) {
                return false;
            }
        }
        node.myData = data;
        return true;
    }
    
    
    private TreeNode search(TreeNode root, String name) {
        TreeNode node = root.myLeftChild;
        while ((node != null) && (node.myData.getName().compareTo(name) < 0)) {
            node = node.myNext;
        }
        if (node.myData.getName().equals(name)) {
            return node;
        }
        return null;
    }
    
   
    /**
     * searchs a node specified with <code>name</code> among
     *  immediate descendants of <code> root</code>
     * 
     * @param root
     * @param name
     * @return a found node or new <code>TreeNode</code> if not found 
     */
    private TreeNode searchAdd(TreeNode root, String name) {
        TreeNode node = root.myLeftChild;
        TreeNode prev = null;
        while ((node != null) && (node.myData.getName().compareTo(name) < 0)) {
            prev = node;
            node = node.myNext;
        }
        //no children
        if ((node == null) && (prev == null)){
            TreeNode newNode = null;
            newNode = new TreeNode(new NodeData(name, 0, true, this, compareFileContent), root);
            root.myLeftChild = newNode;
            return newNode;
        }
        
        //out of children, not found        
        if (node == null) {
            prev.myNext = new TreeNode(new NodeData(name, 0, true, this, compareFileContent), root);
//            prev.myNext = new TreeNode(new NodeData(name, 0, true, this), root);
            return prev.myNext;
        }
        
//        found node
        if (node.myData.getName().equals(name)) {
//            System.out.println("found");
            return node;
        }
        
//      insert into head
        if ((node != null) && (prev == null)) {
            root.myLeftChild = new TreeNode(new NodeData(name, 0, true, this, compareFileContent), root);
//            root.myLeftChild = new TreeNode(new NodeData(name, 0, true, this), root);
            root.myLeftChild.myNext = node;
            return root.myLeftChild;
        }
//        not found, insert into the middle
//        TreeNode newNode = new TreeNode(new NodeData(name, 0, true, this), root);
        TreeNode newNode = null;
        newNode = new TreeNode(new NodeData(name, 0, true, this, compareFileContent), root);
        newNode.myNext = prev.myNext;
        prev.myNext = newNode;
        return newNode;
    }
    
    public List<NodeData> getSubElements(String path) {
        TreeNode node = searchByPath(path);
        List<NodeData> result = new ArrayList<NodeData>();
        TreeNode child  = node.myLeftChild;
        while (child != null) {
            result.add(child.myData);
            if (child.myLeftChild != null) {
                viewSubElements(child, result);
            }
            child = child.myNext;
        }
        return result;
    }
    
    private void viewSubElements(TreeNode node, List<NodeData> data) {
        TreeNode child  = node.myLeftChild;
        while (child != null) {
            data.add(child.myData);
            if (child.myLeftChild != null) {
                viewSubElements(child, data);
            }
            child = child.myNext;
        }
    }
    
    private TreeNode searchByPath(String path) {
        //System.out.println(path);
        Deque<String> dPath = Path.stringToDeque(path);
        TreeNode cur = myRoot;
        while (!dPath.isEmpty()) {
            cur = cur.myLeftChild;
            while (!dPath.getFirst().equals(cur.myData.getName())) {
                cur = cur.myNext;
            }
            dPath.removeFirst();
        }
        return cur;
    }
    
    private void visitTree(TreeNode node, LeafStrategy strategy) {
        TreeNode child = node.myLeftChild;
        while (child != null) {
            strategy.accept(child);
            if (child.myLeftChild != null) {
                visitTree(child, strategy);
            }
            child = child.myNext;
        }
    }
    
    
    
    public String toString() {
        StringBuilder result = printSubtree(myRoot, new StringBuilder());
        result.deleteCharAt(result.length() - 1);
        return result.toString();
    }
    
    private StringBuilder printSubtree(TreeNode node, StringBuilder path) {
        StringBuilder result = new StringBuilder();
        TreeNode child;
        path.append(node.myData.getName());
        result.append(path);
        result.append("\n");
        if (node.myLeftChild != null) {
            child = node.myLeftChild;
            while (child != null) {
                result.append(printSubtree(child, new StringBuilder(path + "/")));
                child = child.myNext;
            }
        }
        return result;
    }
    
    public String getPath() {
        return myPath;
    }

    public void addAtomicDiff(Difference d, List<Difference> atomic, boolean isDel) {
        if (d.isDirectory()) {
            TreeNode node = searchByPath(d.getPath());
            getAtomic(node, atomic, d.getOperation(), isDel);
        } else {
            atomic.add(d);
        }
    }

    public void addAtomicDiffs(List<Difference> diffs, List<Difference> atomic, boolean isDel) {
        for (Difference d: diffs) {
            if (d.isDirectory()) {
                TreeNode node = searchByPath(d.getPath());
                getAtomic(node, atomic, d.getOperation(), isDel);
            } else {
                atomic.add(d);
            }
        }
    }
    
    private void getAtomic(TreeNode cur, List<Difference> atomic, Operation operation, boolean isDel) {
        if (!isDel) {
            atomic.add(new Difference(cur.getPath(), operation, cur.myLeftChild != null));
        }
        TreeNode child = cur.myLeftChild;
        while (child != null) {
            if (child.myLeftChild != null) {
                getAtomic(child, atomic, operation, isDel);
            } else {
                atomic.add(new Difference(child.getPath(), operation, child.myLeftChild != null));
            }
            child = child.myNext;
        }
        if (isDel) {
            atomic.add(new Difference(cur.getPath(), operation, cur.myLeftChild != null));
        }
    }
    
    private class SetFileContentStrategy implements LeafStrategy{
        private boolean compare;
        public SetFileContentStrategy(boolean compare) {
            this.compare = compare;
        }
        
        public void accept(TreeNode node) {
            node.setCompareFileContent(compare);
            
        }
        
    }

}
